package com.media.service.impl;

import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.base.model.R;
import com.media.feignclient.MediaSearchClient;
import com.media.feignclient.MessageClient;
import com.media.mapper.MediaMapper;
import com.media.mapper.PublishRecodeMapper;
import com.media.mapper.TagMapper;
import com.media.model.*;
import com.media.model.dto.PieDataDto;
import com.media.service.MediaService;
import com.media.service.PublishRecodeService;
import com.media.service.PublishService;
import com.base.utils.VideoUtil;
import com.j256.simplemagic.ContentInfo;
import com.j256.simplemagic.ContentInfoUtil;
import com.media.utils.RedisUtil;
import io.minio.*;
import io.minio.errors.*;
import io.seata.spring.annotation.GlobalTransactional;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.codec.digest.DigestUtils;
import org.apache.commons.io.IOUtils;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.io.*;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.sql.Timestamp;
import java.util.*;

/**
 * @Author: Lenovo
 * @Description: TODO
 * @DateTime: 2023/9/14 14:44
 **/
@Slf4j
@Service
public class MediaServiceImpl extends ServiceImpl<MediaMapper, Media> implements MediaService {

    private static final String REDIS_MEDIA_PREFIX = "media_";

    @Autowired
    private MediaSearchClient esClient;

    @Autowired
    private RedisUtil redisUtil;

    @Autowired
    private MediaMapper mediaMapper;

    @Autowired
    private MessageClient messageClient;

    @Autowired
    private MinioClient minioClient;

    @Autowired
    private MediaService mediaService;

    @Autowired
    private PublishService publishService;

    @Autowired
    private PublishRecodeMapper publishRecodeMapper;

    @Autowired
    private PublishRecodeService publishRecodeService;

    @Autowired
    private RedisTemplate redisTemplate;



    private static final String BUCKET_VIDEO = "video";


    /**
     * 用户上传视频信息，视频封面，视频文件
     *
     * @param filePathMedia 视频临时文件路径
     * @param filePathCover 封面临时文件路径
     * @param publish       预发布视频信息
     * @return 字符串
     */
    @Override
    @GlobalTransactional
    public R<String> uploadMedia(String filePathMedia, String filePathCover, Publish publish, String tags) {
        VideoUtil videoUtil = new VideoUtil();
        //根据临时文件的绝对路径创建文件
        File fileMedia = new File(filePathMedia);
        File fileCover = new File(filePathCover);
        try {
            //获取视频MD5值,判断是否已存在。需判断已发布视频和预发布视频是否存在相同MD5文件
            String fileMd5 = getFileMd5(fileMedia);
            boolean containsMd5 = judgeContainsMd5(fileMd5);
            if (!containsMd5) {
                return R.error("您上传的视频已存在,请检查视频是否有误。");
            }
            //填充MD5字段
            publish.setMd5(fileMd5);

            //获取文件名
            String fileNameMedia = fileMedia.getName();
            String fileNameCover = fileCover.getName();
            //根据文件名获取文件拓展名
            String extensionMedia = fileNameMedia.substring(fileNameMedia.lastIndexOf('.'));
            String extensionCover = fileNameCover.substring(fileNameCover.lastIndexOf('.'));
            //调用minioAPI根据文件拓展名获取MimeType,MimeType用于minio文件上传
            String mimeTypeMedia = getMimeType(extensionMedia);
            String mimeTypeCover = getMimeType(extensionCover);

            //通过MD5获取文件在minio中存储的位置,存储位置为/a/b/md5/md5.mp4。a为MD5第一位，b为MD5第二位。通过MD5前两位构建索引，提高文件检索效率。
            //当文件数过多时，可以考虑多用几位。
            assert fileMd5 != null;
            String filePathMediaByMd5 = getFilePathByMd5(fileMd5, extensionMedia);
            String filePathCoverByMd5 = getFilePathByMd5(fileMd5, extensionCover);

            //将文件上传到minio
            boolean resultMedia = this.uploadFileToMinio(filePathMedia, mimeTypeMedia, BUCKET_VIDEO, filePathMediaByMd5);
            //将封面上传到minio中,和视频文件在同一个文件夹下,再取视频时,同时将封面一起取出。
            boolean resultCover = this.uploadFileToMinio(filePathCover, mimeTypeCover, BUCKET_VIDEO, filePathCoverByMd5);
            if (!resultMedia || !resultCover) {
                return R.error("上传文件失败");
            }

            //将视频信息存储到预发布表中
            boolean save = publishService.save(publish);
            if (!save) {
                return R.error("视频信息存储预发布表失败");
            }
            //判断当前视频是否需要转码
            int status = judgeTranscodeByExtension(extensionMedia, publish);
            //将发布记录存储到记录表
            savePublishRecodeByPublish(publish, status);
        } finally {
            //使用try finally来保证不管上述操作是否正确执行，最终都将临时文件删除，防止临时文件占用空间。
            videoUtil.deleteFile(fileMedia, fileCover);
        }
        return R.success("上传文件成功");
    }


    private boolean savePublishRecodeByPublish(Publish publish, Integer status) {
        PublishRecode publishRecode = new PublishRecode();
        BeanUtils.copyProperties(publish, publishRecode);
        publishRecode.setStatus(status);
        publishRecode.setPublishId(publish.getId());
        publishRecode.setUpdateTime(new Timestamp(new Date().getTime()));
        return publishRecodeService.save(publishRecode);
    }

    private Integer judgeTranscodeByExtension(String extension, Publish publish) {
        if (!extension.equals(".mp4")) {
            Message message = new Message();
            message.setMd5(publish.getMd5());
            message.setPublishId(publish.getId());
            message.setSuffix(extension);
            message.setIsTranscode(0);
            messageClient.addMessage(message);
            return -1;
        }
        return 0;
    }

    /**
     * 判断是否已存在相同Md5的视频，Md5相同代表视频相同。
     *
     * @param Md5 视频MD5值
     * @return 是否存在相同MD5值视频, true代表不存在, false代表存在
     */
    @Override
    public boolean judgeContainsMd5(String Md5) {
        //1.判断已发布视频是否存在相同MD5值视频
        LambdaQueryWrapper<Media> mqw = new LambdaQueryWrapper<>();
        mqw.eq(Media::getMd5, Md5);
        Media media = mediaService.getOne(mqw);
        //如果存在则返回false
        if (media != null) {
            return false;
        }
        //否则继续判断预发布表是否存在相同MD5值视频
        LambdaQueryWrapper<Publish> pqw = new LambdaQueryWrapper<>();
        pqw.eq(Publish::getMd5, Md5);
        Publish publish = publishService.getOne(pqw);
        return publish == null;
    }

    /**
     * 上传文件到Minio中，实际的上传文件接口，上传视频和封面都将调用该接口
     *
     * @param filePath   文件路径
     * @param mimeType   上传minio需要传输的参数,代表文件类型,可通过文件的后缀名得到
     * @param bucket     上传到minio的那个桶
     * @param objectName 文件名
     * @return 上传minio是否成功
     */
    @Override
    public boolean uploadFileToMinio(String filePath, String mimeType, String bucket, String objectName) {
        try {
            UploadObjectArgs uploadObjectArgs = UploadObjectArgs.builder()
                    .bucket(bucket)//桶
                    .filename(filePath) //指定本地文件路径
                    .object(objectName)//对象名 放在子目录下
                    .contentType(mimeType)//设置媒体文件类型
                    .build();
            //上传文件
            minioClient.uploadObject(uploadObjectArgs);
            log.debug("上传文件到minio成功,bucket:{},objectName:{}", bucket, objectName);
            return true;
        } catch (Exception e) {
            e.printStackTrace();
            log.error("上传文件出错,bucket:{},objectName:{},错误信息:{}", bucket, objectName, e.getMessage());
        }
        return false;
    }


    /**
     * 下架视频
     *
     * @param mediaId 视频Id
     * @return 视频信息
     */
    @Override
    @Transactional
    public R<String> disableMedia(String mediaId) {
        //1.将视频状态改为禁用
        UpdateWrapper<Media> uw = new UpdateWrapper<>();
        uw.eq("Id", mediaId)
                .set("status", 0);
        boolean update = this.update(uw);
        if (!update) {
            return R.error("视频下架失败");
        }
        //将es中的数据删除
        esClient.deleteMediaByMediaId(mediaId);
        //将redis中的缓存删除
        redisUtil.del(REDIS_MEDIA_PREFIX + mediaId);
        return R.success("视频下架成功");
    }


    /**
     * 查询所有视频信息
     *
     * @return 视频信息
     */
    @Override
    public R<List<Media>> listMedia(Integer page, Integer pageSize, Media media) {
        LambdaQueryWrapper<Media> qw = new LambdaQueryWrapper<>();
        qw.eq(media.getId() != null && !"".equals(media.getId()), Media::getId, media.getId())
                .like(media.getTitle() != null && !media.getTitle().equals(""), Media::getTitle, media.getTitle())
                .eq(media.getCategory() != null, Media::getCategory, media.getCategory())
                .like(media.getUploaderName() != null, Media::getUploaderName, media.getUploaderName());
        Page<Media> mediaPage = new Page<>(page, pageSize);
        this.page(mediaPage, qw);
        Map<String, Object> map = new HashMap<>();
        map.put("count", mediaPage.getTotal());
        return R.success(mediaPage.getRecords(), map);
    }


    @Override
    @Transactional
    public R<String> shelveMedia(String mediaId) {
//        1.将视频状态修改为1
        UpdateWrapper<Media> uw = new UpdateWrapper<>();
        uw.eq("Id", mediaId).set("status", 1);
        boolean update = this.update(uw);
        if (!update) {
            return R.error("视频恢复上架失败");
        }
        //2.向redis中添加缓存
        Media byId = this.getById(mediaId);
        redisTemplate.opsForValue().set(REDIS_MEDIA_PREFIX+mediaId, JSON.toJSONString(byId));
        //3.向es中写入视频数据
        esClient.saveMedia(byId);
        return R.success("视频重新上架成功");
    }

//    @Override
//    @Transactional
//    public R<String> auditAll() {
//        //1.查询预发布表中所有数据
//        List<Publish> list = publishService.list();
//        //2.删除预发布表中所有数据
//        boolean remove = publishService.remove(null);
//        //3.将审核状态表中的所有数据改为已审核
//        //4.向视频表中写入数据
//        List<Media> mediaList=new ArrayList<>();
//        for(Publish publish:list){
//            Media media=new Media();
//            BeanUtils.copyProperties(publish,media);
//            media.setLikes(0);
//            media.setViews(0);
//            media.setComments(0);
//            media.setStatus(1);
//            mediaList.add(media);
//        }
//        mediaService.saveBatch(mediaList);
//        return R.success("123");
//    }

    @Override
    @Transactional
    public R<String> updateMedia(Media media) {
        //1.修改视频表中的数据
        boolean result = this.updateById(media);
        if (!result) {
            return R.error("修改视频数据失败");
        }
        //2.覆盖redis中的内容
        redisTemplate.opsForValue().set(REDIS_MEDIA_PREFIX+media.getId(),JSON.toJSONString(media));
        //3.修改es中的数据
        //相同Id，创建文档时已存在则修改
        esClient.saveMedia(media);
        return R.success("视频数据修改成功");
    }


    @Override
    public File downFileFromMinio(String MD5, String suffix) {
        try {
            String originUrl = getFilePathByMd5(MD5, suffix);
            GetObjectArgs args = GetObjectArgs.builder().bucket(BUCKET_VIDEO).object(originUrl).build();
            InputStream inputStream = minioClient.getObject(args);
            if (inputStream == null) {
                return null;
            }
            File file = File.createTempFile(UUID.randomUUID().toString().replace("-", ""), suffix);
            FileOutputStream outputStream = new FileOutputStream(file);
            IOUtils.copy(inputStream, outputStream);
            return file;
        } catch (ErrorResponseException | InsufficientDataException | InternalException | InvalidKeyException | InvalidResponseException | IOException | NoSuchAlgorithmException | ServerException | XmlParserException e) {
            e.printStackTrace();
        }
        return null;
    }

    @Override
    public R<Media> getVideoByVideoId(String videoId) {
        Media byId = mediaService.getById(videoId);
        if (byId == null) {
            return R.error("未查询到视频Id");
        }
        return R.success(byId);
    }




    /**
     * 获取文件Md5值
     *
     * @param file 文件
     * @return 文件MD5值
     */
    private String getFileMd5(File file) {
        try (FileInputStream fileInputStream = new FileInputStream(file)) {
            return DigestUtils.md5Hex(fileInputStream);
        } catch (Exception e) {
            e.printStackTrace();
            return null;
        }
    }

    /**
     * 根据扩展名获取文件MimeType
     *
     * @param extension 文件拓展名
     * @return mimeType
     */
    private String getMimeType(String extension) {
        if (extension == null) {
            extension = "";
        }
        //根据扩展名取出mimeType
        ContentInfo extensionMatch = ContentInfoUtil.findExtensionMatch(extension);
        String mimeType = MediaType.APPLICATION_OCTET_STREAM_VALUE;//通用mimeType，字节流
        if (extensionMatch != null) {
            mimeType = extensionMatch.getMimeType();
        }
        return mimeType;

    }


    /**
     * 通过Md5得到文件存储minio中的地址
     *
     * @param fileMd5 文件id即md5值
     * @param fileExt 文件扩展名
     * @return 文件名
     */
    private String getFilePathByMd5(String fileMd5, String fileExt) {
        return fileMd5.charAt(0) + "/" + fileMd5.charAt(1) + "/" + fileMd5 + "/" + fileMd5 + fileExt;
    }

}
